﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using l1cs.server.datatables;
using l1cs.server.encryptions;
using l1cs.server.model;
using l1cs.server.model.Instance;
using l1cs.server.serverpackets;
using l1cs.server.types;
using l1cs.server.utils;

namespace l1cs.server
{
    public class ClientThread : PacketOutput
    {
        private static Logger _log = Logger.getLogger( typeof( ClientThread ).FullName );

        private NetworkStream stream;

        private StreamReader _in;

        private StreamWriter _out;

        private PacketHandler _handler;

        private Account _account;

        private L1PcInstance _activeChar;

        private String _ip;

        private String _hostname;

        private Socket _csocket;

        private int _loginStatus = 0;

        protected ClientThread() { }

        public ClientThread( Socket socket, IPAddress address, IPHostEntry hostInfo )
        {
            _csocket = socket;
            _ip = address.ToString();
            if ( Config.HOSTNAME_LOOKUPS )
            {
                _hostname = Dns.GetHostEntry( address ).HostName;
            }
            else
            {
                _hostname = _ip;
            }
            stream = new NetworkStream( socket );
            _in = new StreamReader( stream, Encoding.GetEncoding( Config.CLIENT_LANGUAGE_CODE ) );
            _out = new StreamWriter( stream, Encoding.GetEncoding( Config.CLIENT_LANGUAGE_CODE ) );

            // PacketHandler 初期設定
            _handler = new PacketHandler( this );
        }

        public String getIp()
        {
            return _ip;
        }

        public String getHostname()
        {
            return _hostname;
        }

        // ClientThreadによる一定間隔自動セーブを制限する為のフラグ（true:制限 false:制限無し）
        // 現在はC_LoginToServerが実行された際にfalseとなり、
        // C_NewCharSelectが実行された際にtrueとなる
        private bool _charRestart = true;

        public void CharReStart( bool flag )
        {
            _charRestart = flag;
        }

        private LineageKeys _clkey;

        private byte[] readPacket()
        {
            byte[] getData = new byte[1];
            try
            {
                stream.Read( getData, 0, getData.Length );
                int hiByte = Convert.ToInt32( getData[0] );
                stream.Read( getData, 0, getData.Length );
                int loByte = Convert.ToInt32( getData[0] );
                if ( loByte < 0 )
                {
                    throw new Exception();
                }
                int dataLength = ( loByte * 256 + hiByte ) - 2;

                List<byte> data = new List<byte>();

                int readSize = 0;

                for ( int i = 0; i < dataLength; i++ )
                {
                    stream.Read( getData, 0, getData.Length );
                    data.Add( getData[0] );
                    readSize++;
                }

                if ( readSize != dataLength )
                {
                    string logstr = "Incomplete Packet is sent to the server, closing connection.\n";
                    logstr += String.Format( "readSize : dataLength = {0} : {1}", readSize, dataLength );
                    if ( readSize > 0 )
                    {
                        logstr += String.Format( "[受信データ<<<< Opcode={0} {1}]\n{2}",
                            data[0], data, ByteArrayUtil.dumpToString( data.ToArray() ) );
                    }
                    else
                    {
                        logstr += "[受信データが0バイトです]";
                    }
                    _log.warning( logstr );
                    throw new Exception();
                }

                if ( frmMain.isEncode )
                {
                    return LineageEncryption.decrypt( data.ToArray(), dataLength, _clkey );
                }
                else
                {
                    return data.ToArray();
                }
            }
            catch ( Exception e )
            {
                _log.info( e.Message + "\n" + e.StackTrace );
                throw;
            }
        }

        private DateTime _lastSavedTime = DateTime.Now;

        private DateTime _lastSavedTime_inventory = DateTime.Now;

        private void doAutoSave()
        {
            if ( _activeChar == null || _charRestart )
            {
                return;
            }
            // キャラクター情報
            DateTime now = DateTime.Now;
            if ( Config.AUTOSAVE_INTERVAL < ( now - _lastSavedTime ).TotalSeconds )
            {
                _activeChar.save();
                _lastSavedTime = now;
            }

            // 所持アイテム情報
            if ( Config.AUTOSAVE_INTERVAL_INVENTORY < ( now - _lastSavedTime ).TotalSeconds )
            {
                _activeChar.saveInventory();
                _lastSavedTime_inventory = now;
            }
        }

        public void threadSub( Object state )
        {
            Console.WriteLine( "(" + _hostname + ")がサーバーに接続しました。" );
            Console.WriteLine( String.Format( "利用メモリ: {0:f2}MB", GC.GetTotalMemory( false ) / 1024.0 / 1024.0 ) );
            Console.WriteLine( "クライアント接続待機中..." );

            Socket socket = _csocket;
            /*
             * クライアントからのパケットをある程度制限する。 
             * 理由：不正の誤検出が多発する恐れがあるため
             * ex1.サーバに過負荷が掛かっている場合、
             *     負荷が落ちたときにクライアントパケットを一気に処理し、結果的に不正扱いとなる。
             * ex2.サーバ側のネットワーク（下り）にラグがある場合、
             *     クライアントパケットが一気に流れ込み、結果的に不正扱いとなる。
             * ex3.クライアント側のネットワーク（上り）にラグがある場合、以下同様。
             * 
             * 無制限にする前に不正検出方法を見直す必要がある。
             */
            HcPacket movePacket = new HcPacket( this, M_CAPACITY );
            HcPacket hcPacket = new HcPacket( this, H_CAPACITY );
            ThreadPool.QueueUserWorkItem( new WaitCallback( movePacket.threadSub ) );
            ThreadPool.QueueUserWorkItem( new WaitCallback( hcPacket.threadSub ) );

            // 自動切断までの時間（単位:ms）
            ClientThreadObserver observer =
                    new ClientThreadObserver( this, Config.AUTOMATIC_KICK * 60 * 1000 );

            // クライアントスレッドの監視
            if ( Config.AUTOMATIC_KICK > 0 )
            {
                observer.start();
            }

            stream.Write( Config.FIRST_PACKET, 0, Config.FIRST_PACKET_LEN );
            stream.Flush();
#if DEBUG
            string logstr = string.Format( "[送信データ>>>>]\n{0}", ByteArrayUtil.dumpToString( Config.FIRST_PACKET.ToArray() ) );
            _log.info( logstr );
#endif
            _clkey = LineageEncryption.initKeys( socket, Config.SEED );

            while ( true )
            {
                doAutoSave();

                byte[] data = null;
                try
                {
                    data = readPacket();
#if DEBUG
                    String strOpcode;
                    cOpCode.TryGetValue( data[0], out strOpcode );
                    String consoleStr = String.Format( "[受信データ<<<< Opcode={0} {1}]\n{2}",
                        data[0], strOpcode, ByteArrayUtil.dumpToString( data.ToArray() ) );
                    _log.info( consoleStr );
#endif
                }
                catch ( Exception e )
                {
                    _log.info( e.Message + "\n" + e.StackTrace );
                    break;
                }

                int opcode = data[0] & 0xFF;

                // 多重ログイン対策
                if ( opcode == Opcodes.C_OPCODE_COMMONCLICK
                        || opcode == Opcodes.C_OPCODE_CHANGECHAR )
                {
                    _loginStatus = 1;
                }
                if ( opcode == Opcodes.C_OPCODE_LOGINTOSERVER )
                {
                    if ( _loginStatus != 1 )
                    {
                        continue;
                    }
                }
                if ( opcode == Opcodes.C_OPCODE_LOGINTOSERVEROK
                        || opcode == Opcodes.C_OPCODE_RETURNTOLOGIN )
                {
                    _loginStatus = 0;
                }

                if ( opcode != Opcodes.C_OPCODE_KEEPALIVE )
                {
                    // C_OPCODE_KEEPALIVE以外の何かしらのパケットを受け取ったらObserverへ通知
                    observer.packetReceived();
                }
                // nullの場合はキャラクター選択前なのでOpcodeの取捨選択はせず全て実行
                if ( _activeChar == null )
                {
                    _handler.handlePacket( data, _activeChar );
                    continue;
                }

                // 以降、PacketHandlerの処理状況がClientThreadに影響を与えないようにする為の処理
                // 目的はOpcodeの取捨選択とClientThreadとPacketHandlerの切り離し

                // 破棄してはいけないOpecode群
                // リスタート、アイテムドロップ、アイテム削除
                if ( opcode == Opcodes.C_OPCODE_CHANGECHAR
                        || opcode == Opcodes.C_OPCODE_DROPITEM
                        || opcode == Opcodes.C_OPCODE_DELETEINVENTORYITEM )
                {
                    _handler.handlePacket( data, _activeChar );
                }
                else if ( opcode == Opcodes.C_OPCODE_MOVECHAR )
                {
                    // 移動はなるべく確実に行う為、移動専用スレッドへ受け渡し
                    movePacket.requestWork( data );
                }
                else
                {
                    // パケット処理スレッドへ受け渡し
                    hcPacket.requestWork( data );
                }
            }

            if ( _activeChar != null )
            {
                quitGame( _activeChar );

                lock ( _activeChar )
                {
                    // キャラクターをワールド内から除去
                    _activeChar.logout();
                    setActiveChar( null );
                }
            }

            // 念のため送信
            sendPacket( new S_Disconnect() );

            LoginController.getInstance().logout( this );
            stream.Close();

            _csocket = null;
            _log.fine( "Server thread[C] stopped" );
            if ( _kick < 1 )
            {
                _log.info( String.Format(
                    "({0}:{1})との接続を終了しました。"
                    , getAccountName(), _hostname ) );
                Console.WriteLine( String.Format( "利用メモリ: {0:f2}MB", GC.GetTotalMemory( false ) / 1024.0 / 1024.0 ) );
                Console.WriteLine( "クライアント接続待機中..." );
            }
            return;
        }

        private int _kick = 0;

        public void kick()
        {
            sendPacket( new S_Disconnect() );
            _kick = 1;
            _out.Close();
            _in.Close();
        }

        private const int M_CAPACITY = 3; // 移動要求を一辺に受け付ける最大容量
        private const int H_CAPACITY = 2;// 行動要求を一辺に受け付ける最大容量

        // キャラクターの行動処理スレッド
        class HcPacket
        {
            private Queue _queue;

            private PacketHandler _handler;

            private ClientThread _clt;

            public HcPacket( ClientThread clt )
            {
                _clt = clt;
                _queue = Queue.Synchronized( new Queue() );
                _handler = new PacketHandler( clt );
            }

            /// <summary>
            /// 固定容量でキューを作成する。
            /// というのが仕様だけど、c#では自動拡張
            /// Java:LinkedBlockingQueue<byte[]>(capacity);
            /// </summary>
            /// <param name="clt"></param>
            /// <param name="capacity"></param>
            public HcPacket( ClientThread clt, int capacity )
            {
                _clt = clt;
                _queue = Queue.Synchronized( new Queue() );
                _handler = new PacketHandler( clt );
            }

            public void requestWork( byte[] data )
            {
                _queue.Enqueue( data );
            }

            public void threadSub( Object state )
            {
                byte[] data;
                while ( _clt._csocket != null )
                {
                    if ( _queue.Count != 0 )
                    {
                        data = (byte[])_queue.Dequeue();

                        _handler.handlePacket( data, _clt._activeChar );
                    }
                    Thread.Sleep( 10 );
                }
            }
        }

        //private static Timer _observerTimer = new Timer();

        // クライアントスレッドの監視タイマー
        class ClientThreadObserver
        {
            private int _checkct = 1;

            private int _disconnectTimeMillis;

            private ClientThread _clt;

            public ClientThreadObserver( ClientThread clt, int disconnectTimeMillis )
            {
                _clt = clt;
                _disconnectTimeMillis = disconnectTimeMillis;
            }

            Timer tmr;

            public void start()
            {
                tmr = new Timer( new TimerCallback( run ), null, 0, _disconnectTimeMillis );
            }

            public void run( Object state )
            {
                if ( _clt._csocket == null )
                {
                    tmr.Dispose();
                    return;
                }

                if ( _checkct > 0 )
                {
                    _checkct = 0;
                    return;
                }

                if ( _clt._activeChar == null // キャラクター選択前
                  || _clt._activeChar != null
                 && !_clt._activeChar.isPrivateShop() )
                { // 個人商店中
                    _clt.kick();
                    _log.warning( String.Format(
                        "一定時間応答が得られなかった為({0}))との接続を強制切断しました。",
                        _clt._hostname ) );
                    tmr.Dispose();
                    return;
                }
            }

            public void packetReceived()
            {
                _checkct++;
            }
        }

        public void sendPacket( ServerBasePacket packet )
        {
            lock ( this )
            {
                try
                {
                    byte[] abyte0 = packet.getContent();
#if DEBUG
                    String opcode;
                    if ( abyte0.Length == 0 )
                    {
                        _log.info( string.Format( "[送信データが0バイトです:{0}]", packet.getType() ) );
                        return;

                    }
                    sOpCode.TryGetValue( abyte0[0], out opcode );
                    String consoleStr = String.Format( "[送信データ>>>> Opcode={0} {1}]\n{2}",
                        abyte0[0], packet.getType(), ByteArrayUtil.dumpToString( abyte0 ) );
                    _log.info( consoleStr );
#endif
                    if ( frmMain.isEncode )
                    {
                        char[] ac = new char[abyte0.Length];
                        ac = UChar8.fromArray( abyte0 );
                        ac = LineageEncryption.encrypt( ac, _clkey );
                        abyte0 = UByte8.fromArray( ac );
                    }
#if DEBUG
                    consoleStr = String.Format( "[送信データ(暗号)>>>>]\n{0}",
                        ByteArrayUtil.dumpToString( abyte0 ) );
                    _log.info( consoleStr );
#endif
                    int j = abyte0.Length + 2;
                    byte[] senddata = new byte[j];
                    senddata[0] = (byte)( j & 0xff );
                    senddata[1] = (byte)( j >> 8 & 0xff );
                    Array.Copy( abyte0, 0, senddata, 2, abyte0.Length );
                    stream.Write( senddata, 0, senddata.Length );
                    stream.Flush();
                }
                catch ( Exception e )
                {
                    _log.info( e.Message + "\n" + e.StackTrace );
                }
            }
        }

        public void close()
        {
            _csocket.Close();
        }

        /// <summary>
        /// アクティブキャラクターを設定する。
        /// </summary>
        /// <param name="pc"></param>
        public void setActiveChar( L1PcInstance pc )
        {
            _activeChar = pc;
        }

        public L1PcInstance getActiveChar()
        {
            return _activeChar;
        }

        public void setAccount( Account account )
        {
            _account = account;
        }

        public Account getAccount()
        {
            return _account;
        }

        public String getAccountName()
        {
            if ( _account == null )
            {
                return null;
            }
            return _account.getName();
        }

        public static void quitGame( L1PcInstance pc )
        {
            // 死亡していたら街に戻し、空腹状態にする
            if ( pc.isDead() )
            {
                int[] loc = Getback.GetBack_Location( pc, true );
                pc.setX( loc[0] );
                pc.setY( loc[1] );
                pc.setMap( (short)loc[2] );
                pc.setCurrentHp( pc.getLevel() );
                pc.set_food( 40 );
            }

            // トレードを中止する
            if ( pc.getTradeID() != 0 )
            { // トレード中
                L1Trade trade = new L1Trade();
                trade.TradeCancel( pc );
            }

            // 決闘を中止する
            if ( pc.getFightId() != 0 )
            {
                pc.setFightId( 0 );
                L1PcInstance fightPc = (L1PcInstance)L1World.getInstance()
                        .findObject( pc.getFightId() );
                if ( fightPc != null )
                {
                    fightPc.setFightId( 0 );
                    fightPc.sendPackets( new S_PacketBox( S_PacketBox.MSG_DUEL,
                            0, 0 ) );
                }
            }

            // パーティーを抜ける
            if ( pc.isInParty() )
            { // パーティー中
                pc.getParty().leaveMember( pc );
            }

            // チャットパーティーを抜ける
            if ( pc.isInChatParty() )
            { // チャットパーティー中
                pc.getChatParty().leaveMember( pc );
            }

            // ペットをワールドマップ上から消す
            // サモンの表示名を変更する
            //Object[] petList = pc.getPetList().values().toArray();
            foreach ( Object petObject in pc.getPetList().Values )
            {
                if ( petObject is L1PetInstance )
                {
                    L1PetInstance pet = (L1PetInstance)petObject;
                    pet.dropItem();
                    pc.getPetList().Remove( pet.getId() );
                    pet.deleteMe();
                }
                if ( petObject is L1SummonInstance )
                {
                    L1SummonInstance summon = (L1SummonInstance)petObject;
                    foreach ( L1PcInstance visiblePc in L1World.getInstance()
                            .getVisiblePlayer( summon ) )
                    {
                        visiblePc.sendPackets( new S_SummonPack( summon, visiblePc,
                                false ) );
                    }
                }
            }

            // マジックドールをワールドマップ上から消す
            //Object[] dollList = pc.getDollList().values().toArray();
            foreach ( Object dollObject in pc.getDollList().Values )
            {
                L1DollInstance doll = (L1DollInstance)dollObject;
                doll.deleteDoll();
            }

            // 従者をワールドマップ上から消し、同地点に再出現させる
            //Object[] followerList = pc.getFollowerList().values().toArray();
            foreach ( Object followerObject in pc.getFollowerList().Values )
            {
                L1FollowerInstance follower = (L1FollowerInstance)followerObject;
                follower.setParalyzed( true );
                follower.spawn( follower.getNpcTemplate().get_npcId(),
                        follower.getX(), follower.getY(), follower.getHeading(),
                        follower.getMapId() );
                follower.deleteMe();
            }

            // エンチャントをDBのcharacter_buffに保存する
            CharBuffTable.DeleteBuff( pc );
            CharBuffTable.SaveBuff( pc );
            pc.clearSkillEffectTimer();

            // pcのモニターをstopする。
            pc.stopEtcMonitor();
            // オンライン状態をOFFにし、DBにキャラクター情報を書き込む
            pc.setOnlineStatus( 0 );
            pc.save();
            pc.saveInventory();
        }
#if DEBUG
        // ここから先デバッグ用
        static Dictionary<int, String> cOpCode = new Dictionary<int, string>();
        static Dictionary<int, String> sOpCode = new Dictionary<int, string>();
        static ClientThread()
        {
            cOpCode[0] = "C_OPCODE_BOOKMARK";
            cOpCode[3] = "C_OPCODE_FIGHT";
            cOpCode[4] = "C_OPCODE_KEEPALIVE";
            cOpCode[5] = "C_OPCODE_ATTACK";
            cOpCode[6] = "C_OPCODE_CHANGEHEADING";
            cOpCode[7] = "C_OPCODE_PICKUPITEM";
            cOpCode[8] = "C_OPCODE_SHOP";
            cOpCode[10] = "C_OPCODE_DELBUDDY";
            cOpCode[11] = "C_OPCODE_LEAVEPARTY";
            cOpCode[14] = "C_OPCODE_CHARACTERCONFIG";
            cOpCode[15] = "C_OPCODE_MOVECHAR";
            cOpCode[16] = "C_OPCODE_CHANGECHAR";
            cOpCode[17] = "C_OPCODE_PRIVATESHOPLIST";
            cOpCode[18] = "C_OPCODE_CHAT";
            cOpCode[19] = "C_OPCODE_BOARDREAD";
            cOpCode[20] = "C_OPCODE_TRADEADDITEM";
            cOpCode[22] = "C_OPCODE_PROPOSE";
            cOpCode[23] = "C_OPCODE_HIRESOLDIER";
            cOpCode[24] = "C_OPCODE_BOARD";
            cOpCode[25] = "C_OPCODE_LOGINTOSERVEROK";
            cOpCode[26] = "C_OPCODE_ENTERPORTAL";
            cOpCode[27] = "C_OPCODE_LEAVECLANE";
            cOpCode[29] = "C_OPCODE_CALL";
            cOpCode[30] = "C_OPCODE_TRADE";
            cOpCode[31] = "C_OPCODE_SKILLBUYOK";
            cOpCode[32] = "C_OPCODE_DELEXCLUDE";
            cOpCode[33] = "C_OPCODE_SHIP";
            cOpCode[34] = "C_OPCODE_CLIENTVERSION";
            cOpCode[38] = "C_OPCODE_EXTCOMMAND";
            cOpCode[41] = "C_OPCODE_TRADEADDCANCEL";
            cOpCode[42] = "C_OPCODE_DRAWAL";
            cOpCode[46] = "C_OPCODE_COMMONCLICK";
            cOpCode[47] = "C_OPCODE_SELECTTARGET";
            cOpCode[50] = "C_OPCODE_NEWCHAR";
            cOpCode[51] = "C_OPCODE_FIX_WEAPON_LIST";
            cOpCode[52] = "C_OPCODE_DROPITEM";
            cOpCode[54] = "C_OPCODE_DELETECHAR";
            cOpCode[56] = "C_OPCODE_ADDBUDDY";
            cOpCode[57] = "C_OPCODE_WHO";
            cOpCode[60] = "C_OPCODE_BOARDDELETE";
            cOpCode[61] = "C_OPCODE_TRADEADDOK";
            cOpCode[62] = "C_OPCODE_CREATECLAN";
            cOpCode[63] = "C_OPCODE_ATTR";
            cOpCode[64] = "C_OPCODE_ARROWATTACK";
            cOpCode[65] = "C_OPCODE_NPCACTION";
            cOpCode[66] = "C_OPCODE_TITLE";
            cOpCode[68] = "C_OPCODE_DEPOSIT";
            cOpCode[69] = "C_OPCODE_DELETEINVENTORYITEM";
            cOpCode[70] = "C_OPCODE_CHECKPK";
            cOpCode[72] = "C_OPCODE_BANPARTY";
            cOpCode[73] = "C_OPCODE_CLAN";
            cOpCode[75] = "C_OPCODE_DOOR";
            cOpCode[76] = "C_OPCODE_PLEDGE";
            cOpCode[77] = "C_OPCODE_PARTY";
            cOpCode[78] = "C_OPCODE_RANK";
            cOpCode[79] = "C_OPCODE_TELEPORT";
            cOpCode[80] = "C_OPCODE_CHARRESET";
            cOpCode[82] = "C_OPCODE_RESTART";
            cOpCode[83] = "C_OPCODE_PETMENU";
            cOpCode[84] = "C_OPCODE_BOARDWRITE";
            cOpCode[85] = "C_OPCODE_GIVEITEM";
            cOpCode[87] = "C_OPCODE_BOARDBACK";
            cOpCode[89] = "C_OPCODE_LOGINTOSERVER";
            cOpCode[92] = "C_OPCODE_CHATWHISPER";
            cOpCode[93] = "C_OPCODE_SKILLBUY";
            cOpCode[94] = "C_OPCODE_JOINCLAN";
            cOpCode[95] = "C_OPCODE_RETURNTOLOGIN";
            cOpCode[98] = "C_OPCODE_CHANGEWARTIME";
            cOpCode[101] = "C_OPCODE_WAR";
            cOpCode[103] = "C_OPCODE_BANCLAN";
            cOpCode[104] = "C_OPCODE_RESULT";
            cOpCode[109] = "C_OPCODE_BUDDYLIST";
            cOpCode[110] = "C_OPCODE_TAXRATE";
            cOpCode[111] = "C_OPCODE_USEPETITEM";
            cOpCode[112] = "C_OPCODE_SELECTLIST";
            cOpCode[113] = "C_OPCODE_LOGINPACKET";
            cOpCode[114] = "C_OPCODE_QUITGAME";
            cOpCode[115] = "C_OPCODE_CHATGLOBAL";
            cOpCode[116] = "C_OPCODE_EXCLUDE";
            cOpCode[118] = "C_OPCODE_NPCTALK";
            cOpCode[119] = "C_OPCODE_USEITEM";
            cOpCode[120] = "C_OPCODE_EMBLEM";
            cOpCode[121] = "C_OPCODE_EXIT_GHOST";
            cOpCode[124] = "C_OPCODE_AMOUNT";
            cOpCode[125] = "C_OPCODE_FISHCLICK";
            cOpCode[127] = "C_OPCODE_MAIL";
            cOpCode[128] = "C_OPCODE_BOOKMARKDELETE";
            cOpCode[129] = "C_OPCODE_USESKILL";
            cOpCode[130] = "C_OPCODE_CREATEPARTY";
            cOpCode[131] = "C_OPCODE_CAHTPARTY";
            sOpCode[0] = "S_OPCODE_REMOVE_OBJECT";
            sOpCode[1] = "S_OPCODE_CHARPACK ";
            sOpCode[1] += "S_OPCODE_DROPITEM";
            sOpCode[2] = "S_OPCODE_POLY";
            sOpCode[3] = "S_OPCODE_SYSMSG ";
            sOpCode[3] += "S_OPCODE_GLOBALCHAT";
            sOpCode[6] = "S_OPCODE_DOACTIONGFX";
            sOpCode[7] = "S_OPCODE_EMBLEM";
            sOpCode[8] = "S_OPCODE_INVLIST";
            sOpCode[9] = "S_OPCODE_ITEMNAME";
            sOpCode[10] = "S_OPCODE_POISON";
            sOpCode[11] = "S_OPCODE_TELEPORT";
            sOpCode[12] = "S_OPCODE_SHOWSHOPSELLLIST";
            sOpCode[13] = "S_OPCODE_CHARVISUALUPDATE";
            sOpCode[14] = "S_OPCODE_USEMAP";
            sOpCode[15] = "S_OPCODE_CHANGEHEADING";
            sOpCode[17] = "S_OPCODE_BLESSOFEVA";
            sOpCode[18] = "S_OPCODE_SELECTLIST";
            sOpCode[19] = "S_OPCODE_OWNCHARSTATUS2";
            sOpCode[20] = "S_OPCODE_SKILLBRAVE";
            sOpCode[21] = "S_OPCODE_TRADEADDITEM";
            sOpCode[22] = "S_OPCODE_INVIS";
            sOpCode[24] = "S_OPCODE_SHOWRETRIEVELIST";
            sOpCode[25] = "S_OPCODE_ITEMAMOUNT ";
            sOpCode[25] += "S_OPCODE_ITEMSTATUS";
            sOpCode[26] = "S_OPCODE_WARTIME";
            sOpCode[27] = "S_OPCODE_CHARRESET";
            sOpCode[28] = "S_OPCODE_ADDSKILL";
            sOpCode[29] = "S_OPCODE_NEWCHARWRONG";
            sOpCode[31] = "S_OPCODE_WEATHER";
            sOpCode[32] = "S_OPCODE_CHARTITLE";
            sOpCode[33] = "S_OPCODE_ADDITEM";
            sOpCode[34] = "S_OPCODE_HPUPDATE";
            sOpCode[35] = "S_OPCODE_ATTACKPACKET";
            sOpCode[37] = "S_OPCODE_SHOWHTML";
            sOpCode[38] = "S_OPCODE_CHANGENAME";
            sOpCode[39] = "S_OPCODE_NEWMASTER";
            sOpCode[41] = "S_OPCODE_DISCONNECT";
            sOpCode[43] = "S_OPCODE_LIQUOR";
            sOpCode[44] = "S_OPCODE_RESURRECTION";
            sOpCode[45] = "S_OPCODE_PUTSOLDIER";
            sOpCode[46] = "S_OPCODE_SHOWSHOPBUYLIST";
            sOpCode[47] = "S_OPCODE_WHISPERCHAT";
            sOpCode[48] = "S_OPCODE_SKILLBUY";
            sOpCode[49] = "S_OPCODE_SKILLHASTE";
            sOpCode[50] = "S_OPCODE_NPCSHOUT";
            sOpCode[51] = "S_OPCODE_DEXUP";
            sOpCode[52] = "S_OPCODE_SPMR";
            sOpCode[53] = "S_OPCODE_TRADE";
            sOpCode[55] = "S_OPCODE_SERVERSTAT";
            sOpCode[56] = "S_OPCODE_NEWCHARPACK";
            sOpCode[57] = "S_OPCODE_DELSKILL";
            sOpCode[58] = "S_OPCODE_GAMETIME";
            sOpCode[59] = "S_OPCODE_OWNCHARSTATUS";
            sOpCode[95] = "S_OPCODE_EXP";
            sOpCode[60] = "S_OPCODE_DEPOSIT";
            sOpCode[61] = "S_OPCODE_SELECTTARGET";
            sOpCode[62] = "S_OPCODE_PACKETBOX ";
            sOpCode[62] += "S_OPCODE_ACTIVESPELLS ";
            sOpCode[62] += "S_OPCODE_SKILLICONGFX";
            sOpCode[63] = "S_OPCODE_LOGINRESULT";
            sOpCode[65] = "S_OPCODE_BLUEMESSAGE";
            sOpCode[66] = "S_OPCODE_COMMONNEWS";
            sOpCode[67] = "S_OPCODE_DRAWAL";
            sOpCode[68] = "S_OPCODE_HIRESOLDIER";
            sOpCode[69] = "S_OPCODE_EFFECTLOCATION";
            sOpCode[70] = "S_OPCODE_TRUETARGET";
            sOpCode[71] = "S_OPCODE_NORMALCHAT";
            sOpCode[72] = "S_OPCODE_HOUSELIST";
            sOpCode[73] = "S_OPCODE_MAPID ";
            sOpCode[73] += "S_OPCODE_UNDERWATER";
            sOpCode[75] = "S_OPCODE_DELETEINVENTORYITEM";
            sOpCode[80] = "S_OPCODE_CHARAMOUNT";
            sOpCode[81] = "S_OPCODE_PARALYSIS";
            sOpCode[82] = "S_OPCODE_ATTRIBUTE";
            sOpCode[83] = "S_OPCODE_SOUND";
            sOpCode[84] = "S_OPCODE_DETELECHAROK";
            sOpCode[85] = "S_OPCODE_TELEPORTLOCK";
            sOpCode[86] = "S_OPCODE_ABILITY";
            sOpCode[87] = "S_OPCODE_PINKNAME";
            sOpCode[89] = "S_OPCODE_SERVERVERSION";
            sOpCode[91] = "S_OPCODE_BOARDREAD";
            sOpCode[92] = "S_OPCODE_MPUPDATE";
            sOpCode[93] = "S_OPCODE_BOARD";
            sOpCode[94] = "S_OPCODE_WAR";
            sOpCode[96] = "S_OPCODE_OWNCHARATTRDEF";
            sOpCode[97] = "S_OPCODE_RESTART";
            sOpCode[98] = "S_OPCODE_SERVERMSG";
            sOpCode[99] = "S_OPCODE_IDENTIFYDESC";
            sOpCode[100] = "S_OPCODE_PINGTIME";
            sOpCode[101] = "S_OPCODE_SKILLSOUNDGFX";
            sOpCode[102] = "S_OPCODE_CHARLIST";
            sOpCode[103] = "S_OPCODE_BOOKMARKS";
            sOpCode[104] = "S_OPCODE_HPMETER";
            sOpCode[105] = "S_OPCODE_YES_NO";
            sOpCode[106] = "S_OPCODE_STRUP";
            sOpCode[107] = "S_OPCODE_ITEMCOLOR";
            sOpCode[110] = "S_OPCODE_CURSEBLIND";
            sOpCode[111] = "S_OPCODE_CASTLEMASTER";
            sOpCode[112] = "S_OPCODE_RANGESKILLS";
            sOpCode[113] = "S_OPCODE_HOUSEMAP";
            sOpCode[114] = "S_OPCODE_SKILLICONSHIELD";
            sOpCode[115] = "S_OPCODE_PRIVATESHOPLIST";
            sOpCode[116] = "S_OPCODE_UNKNOWN1";
            sOpCode[117] = "S_OPCODE_CHARLOCK";
            sOpCode[119] = "S_OPCODE_LAWFUL";
            sOpCode[120] = "S_OPCODE_TAXRATE";
            sOpCode[122] = "S_OPCODE_TRADESTATUS";
            sOpCode[123] = "S_OPCODE_INPUTAMOUNT";
            sOpCode[124] = "S_OPCODE_LIGHT";
            sOpCode[126] = "S_OPCODE_MOVEOBJECT";
            sOpCode[127] = "S_OPCODE_MAIL";
        }
#endif
    }
}
